﻿using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Net;
using System.Reflection;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Permissions;
using System.Threading;
using Microsoft.ApplicationBlocks.Updater;
using Microsoft.Tools.WindowsInstallerXml.Extensions;

using Katersoft.Downloaders;

using DotUpdater.ActivationProcessors;

namespace DotUpdater
{
    public sealed class Updater : IDisposable
    {
        private readonly IDownloader downloader;
        private readonly IActivationProcessor installActivator;
        private readonly FeedBuilder feedBuilder;
        private readonly Func<string, string> checksum;
        private readonly Logger logger;

        private string appDataPath;
        public string AppDataPath 
        { 
            get
            {
                // Could be in "set", but Directory.Exists it slows application startup
//                if (!Directory.Exists(appDataPath)) Directory.CreateDirectory(appDataPath);
                if (string.IsNullOrEmpty(appDataPath)) return Utility.TempBasePath;
                return appDataPath;
            }
            set 
            { 
                appDataPath = value;
            }
        }

        #region Constructors

        public Updater( Logger logger, IDownloader downloader, IActivationProcessor activator, string updatesFeedUrl, 
            Func<string, string> checksumAlgorithm)
        {

            checksum = checksumAlgorithm;
            this.downloader = downloader;
            downloader.DownloadCompleted += DownloadTaskCompleted;
            downloader.DownloadError += DownloadTaskError;
            downloader.DownloadProgress += DownloadTaskProgress;
            installActivator = activator;
            feedBuilder = new FeedBuilder { Url = new Uri(updatesFeedUrl) };
            //this.updatesFeedUrl = updatesFeedUrl;
            this.logger = logger;
        }

        public Updater( Logger logger, IDownloader downloader, IActivationProcessor activator, string updatesFeedUrl)
            : this (logger, downloader, activator, updatesFeedUrl, ChecksumAlgorithms.Md5)
        {
        }

        public Updater(Logger logger, string updatesFeedUrl)
            : this(
                logger,
//                new BitsDownloader("", "",
//                    BG_AUTH_SCHEME.BG_AUTH_SCHEME_BASIC, BG_AUTH_TARGET.BG_AUTH_TARGET_SERVER),
                new HttpDownloader(),
                new WaitForAppExitProcessorSimple(), 
                updatesFeedUrl)
        {
            downloader.logger = logger;
        }

        #endregion Constructors

        /// <summary>
        /// 
        /// </summary>
        /// <returns>null if no new update available</returns>
        /// <exception cref="WebException"></exception>
        /// <exception cref="UpdaterException"></exception>
        /// <exception cref="ArgumentException"></exception>
        public UpdaterTask CheckForUpdates()
        {
            try
            {
                feedBuilder.OpenPrevious(feedBuilder.Url);
            }
            catch (WebException e)
            {
                logger.LogWarning(e.Message);
                throw new UpdaterException("Update error: web", e);
            }
            catch (ArgumentException e)
            {
                logger.LogWarning(e.Message);
                throw new UpdaterException("Update error: URL", e);
            }
            catch (UpdaterException e) // WebException, UpdaterException, ArgumentException
            {
                logger.LogWarning(e.Message);
                throw;
            }

            Assembly a = Assembly.GetCallingAssembly();
            Version curVersion = a.GetName(false).Version;
            if (curVersion.CompareTo(feedBuilder.PreviousAppItem.Version) >= 0) return null;

            LastChecked = DateTime.Now;
            return CreateFromFeed();
        }

        public UpdaterTask CreateFromFeed()
        {
            if (feedBuilder.PreviousAppItem == null)
            {
                try
                {
                    feedBuilder.OpenPrevious(feedBuilder.Url);
                }
                catch (ArgumentException e)
                {
                    logger.LogWarning(e.Message);
                    return null;
                }
                catch (UpdaterException e)
                {
                    logger.LogWarning(e.Message);
                    return null;
                }
            }

            Uri fileUrl = feedBuilder.PreviousAppItem.Url;
            string[] files = new[] { fileUrl.Segments[fileUrl.Segments.Length - 1] };
            string fileBase = fileUrl.AbsoluteUri.Substring(0, fileUrl.AbsoluteUri.Length - files[0].Length);
            UpdaterTask result = new UpdaterTask(AppDataPath, fileBase,
                files, feedBuilder.PreviousAppItem.Id, 
                new string[] { feedBuilder.PreviousAppItem.Digest });
            result.Version = feedBuilder.PreviousAppItem.Version.ToString();
            result.Downloaded = CheckDownloadIntegrity(result);
            return result;
        }

        private readonly TimeSpan downloadTimeout = TimeSpan.FromHours(2);

        public bool Download(UpdaterTask task)
        {
            //string filename = Path.Combine(task.DownloadFilesBase, task.Files[0]);
            if (CheckDownloadIntegrity(task)) return true;

            downloader.Download(task, downloadTimeout);
            return task.Downloaded;
        }

        public void BeginDownload(UpdaterTask task)
        {
            string filename = Path.Combine(task.DownloadFilesBase, task.Files[0]);
            if (File.Exists(filename))
            {
                if (CheckDownloadIntegrity(task))
                {
                    // Check if it's the necessary signal.
                    DownloadTaskCompleted(this, new TaskEventArgs(task));
                    return; // we're done.
                }
                File.Delete(filename);
            }

            downloader.BeginDownload(task);
        }

        public EventHandler<TaskEventArgs> OnDownloadCompleted;
        public EventHandler<DownloadTaskProgressEventArgs> OnDownloadProgress;
        public EventHandler<DownloadTaskErrorEventArgs> OnDownloadError;

        //public event UpdateAvailableEventHandler OnUpdateAvailable;
        //private Task completeTask;

        private void DownloadTaskCompleted(object sender, TaskEventArgs e)
        {
            e.Task.Downloaded = CheckDownloadIntegrity(e.Task);

            if (!e.Task.Downloaded)
            {
                DownloadTaskError(sender, 
                    new DownloadTaskErrorEventArgs(e.Task, new UpdaterException("checksum mismatch")));
            }

            if (OnDownloadCompleted == null) return;
            OnDownloadCompleted(sender, e);
        }

        private bool CheckDownloadIntegrity(UpdaterTask task)
        {
            string filePath = Path.Combine(task.DownloadFilesBase, task.Files[0]);
            return CheckDownloadIntegrity(task, filePath);
        }

        private bool CheckDownloadIntegrity(UpdaterTask task, string filePath)
        {
            if (!File.Exists(filePath)) return false;
            var fileChecksum = checksum(filePath);
            var result = string.Compare(fileChecksum, task.Checksums[0], true) == 0;
            return result;
        }

        private void DownloadTaskError(object sender, DownloadTaskErrorEventArgs e)
        {
            e.Task.Downloaded = false;
            if (OnDownloadError == null) return;
            OnDownloadError(sender, e);
        }

        private void DownloadTaskProgress(object sender, DownloadTaskProgressEventArgs e)
        {
            if (OnDownloadProgress == null) return;
            OnDownloadProgress(sender, e);
        }

        public void Install(UpdaterTask task)
        {
            if (!task.Downloaded)
            {
                if (!Download(task)) throw new ApplicationUpdaterException("Can't download " + task);
            }

            //SaveTask(task);
            installActivator.Init(task);
            installActivator.Execute();
        }

        public DateTime LastChecked { get; set; }

        #region Automatic update - not functional yet

        private int automaticCheckPeriod = -1;
        private Thread checkingThread;

        private void CheckCycle()
        {
            if (automaticCheckPeriod <= 0) return;
        }

        /// <summary>
        /// In minutes. Set to 0 or -1 in order to disable, otherwise enable.
        /// </summary>
        public int AutomaticCheckPeriod
        { 
            get
            {
                return automaticCheckPeriod;
            }
            set
            {
                automaticCheckPeriod = value;
                if (checkingThread == null && automaticCheckPeriod > 0)
                {
                    checkingThread = new Thread(CheckCycle);
                }
            }
        }

        #endregion Automatic update

        #region Persistence

        [Obsolete("Not tested")]
        /// <summary>
        /// Loads all the pending tasks.
        /// </summary>
        public void Load()
        {
            DirectoryInfo rootDir = new DirectoryInfo(
                Path.Combine(AppDataPath, ""));
            foreach (FileInfo fi in rootDir.GetFiles("*.task"))
            {
                this.LoadTask(fi.FullName);
            }
        }

        [Obsolete("Not tested")]
        /// <summary>
        /// Updates the information of an existing stored task.
        /// </summary>
        /// <param name="task">The UpdaterTask instance.</param>
        public void UpdateTask(UpdaterTask task)
        {
            SaveTask(task);
        }

        /// <summary>
        /// The in memory registry storage.
        /// </summary>
        private readonly Dictionary<Guid, UpdaterTask> registry = new Dictionary<Guid, UpdaterTask>();

        [Obsolete("Not tested")]
        /// <summary>
        /// Load the tasks stored in a specified path.
        /// </summary>
        /// <param name="taskFilePath">The base path for the registry storage.</param>
        /// <returns>An UpdaterTask instance.</returns>
        [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
        private UpdaterTask LoadTask(string taskFilePath)
        {
            BinaryFormatter formatter = new BinaryFormatter();
            using (FileStream stream = new FileStream(taskFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                UpdaterTask task = (UpdaterTask)formatter.Deserialize(stream);
                lock (registry)
                {
                    if (task.State != UpdaterTaskState.ACTIVATED && !registry.ContainsKey(task.TaskId))
                    {
                        registry[task.TaskId] = task;
                        
                    }
                    return task;
                }
            }
        }

        [Obsolete("Not tested")]
        /// <summary>
        /// Stores a task in the registry storage.
        /// </summary>
        /// <param name="task">The UpdaterTask instance.</param>
        [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
        [SuppressMessage("Microsoft.Design", "CA1031")]
        private void SaveTask(UpdaterTask task)
        {
            DirectoryInfo rootDir = new DirectoryInfo(AppDataPath);
            string filename = Path.Combine(rootDir.FullName, String.Format(CultureInfo.InvariantCulture, "{0}.task", task.TaskId));
            try
            {
                using (Stream stream = new FileStream(filename, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read))
                {
                    BinaryFormatter formatter = new BinaryFormatter();
                    formatter.Serialize(stream, task);
                }
            }
            catch (Exception ex)
            {
                File.Delete(filename);
                logger.LogAndThrowException(ex);
            }
        }

        [Obsolete("Not tested")]
        /// <summary>
        /// Resumes pending updates that were not completed in a previous session. 
        /// </summary>
        public void ResumePendingUpdates()
        {
            // If there are pending tasks for this application, resume them
            // accordingly to their state
            foreach (UpdaterTask task in registry.Values)
            {
                switch (task.State)
                {
                    case UpdaterTaskState.DOWNLOAD_ERROR:
                    case UpdaterTaskState.DOWNLOADING:
                        downloader.BeginDownload(task);
                        break;
                    case UpdaterTaskState.DOWNLOADED:
                    case UpdaterTaskState.ACTIVATING:
                    case UpdaterTaskState.ACTIVATION_INITIALIZING:
                    case UpdaterTaskState.ACTIVATION_INITIALIZATION_ABORTED:
                    case UpdaterTaskState.ACTIVATION_ERROR:
                        installActivator.Init(task);
                        //activationManagerInstance.SubmitTask(task);
                        if (task.State == UpdaterTaskState.ACTIVATED)
                        {
                            // Finally, unregister the task
                            //RegistryManager.Current.UnRegisterTask(task.TaskId);
                            registry.Remove(task.TaskId);
                        }
                        break;
                }
            }

            //pendingUpdatesResumed = true;
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            //Directory.Delete(appDataPath, true);
        }

        #endregion
    }
}
